/*   Copyright 2010 Jonathan Wagner
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
package com.alienos.collide.reference;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;

import org.javasimon.SimonManager;
import org.javasimon.Split;
import org.javasimon.Stopwatch;

import com.alienos.collide.SpatialManager;
import com.alienos.collide.SpatialObject;
import com.alienos.collide.Transform;
import com.alienos.collide.bounds.BoundingVolume;
import com.alienos.collide.event.CollisionEvent;
import com.alienos.collide.event.CollisionEvent.Type;
import com.alienos.collide.event.CollisionListener;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;

/**
 * Reference spatial manager is a brute-force implementation that does not make use
 * of any sort of broad phase collision culling. The least likely
 * to be affected by edge cases but less efficient for large numbers of
 * collision objects
 * 
 * @author tigeba
 * 
 * @param <I>
 */
public class ReferenceSpatialManager<I> implements SpatialManager<I> {

    private ConcurrentMap<I, SpatialObject<I>> spatials = new ConcurrentHashMap<I, SpatialObject<I>>();
    private CopyOnWriteArrayList<CollisionListener<I>> collisionListeners = new CopyOnWriteArrayList<CollisionListener<I>>();
    private final QueryType collisionMode;

    public ReferenceSpatialManager() {
        collisionMode = QueryType.Intersection;
    }

    public ReferenceSpatialManager(QueryType collisionMode) {
        this.collisionMode = collisionMode;
    }

    @Override
    public synchronized void put(I id, Transform transform, BoundingVolume bounds) {
        Stopwatch stopwatch = SimonManager.getStopwatch("ReferenceSpatialManager.put");
        Split split = stopwatch.start();
        // TODO could probabably use atomic operation on spatial instead of
        // synchronized here
        SpatialObject<I> existing = spatials.get(id);

        if (null != existing) {
            update(existing, transform, bounds);
        } else {
            add(id, transform, bounds);
        }
        split.stop();
    }

    private void add(I id, Transform transform, BoundingVolume bounds) {
        Set<I> collisions = query(bounds, Sets.<I> newHashSet(id), collisionMode);
        spatials.put(id, new SpatialObject<I>(id, transform, bounds, collisions));
        collide(id, collisions);
    }

    private void update(SpatialObject<I> existing, Transform newTransform, BoundingVolume newBounds) {

        Set<I> newC = query(newBounds, Sets.<I> newHashSet(existing.getId()), collisionMode);
        Set<I> oldC = existing.getCollisions();

        Set<I> newCollisions = Sets.difference(newC, oldC);
        Set<I> unCollisions = Sets.difference(oldC, newC);

        spatials.put(existing.getId(), new SpatialObject<I>(existing.getId(), newTransform, newBounds, newC));

        if (newCollisions.size() > 0) {
            collide(existing.getId(), newCollisions);
        }

        if (unCollisions.size() > 0) {
            uncollide(existing.getId(), unCollisions);
        }

    }

    @Override
    public Set<I> query(BoundingVolume bounds, Set<I> exclusions, com.alienos.collide.SpatialManager.QueryType queryType) {
        Stopwatch stopwatch = SimonManager.getStopwatch("ReferenceSpatialManager.query(bounds)");
        Split split = stopwatch.start();
        Set<I> collisions = new HashSet<I>();
        for (SpatialObject<I> o : spatials.values()) {

            if (queryType == QueryType.Intersection) {

                if (bounds.intersects(o.getBounds()) && !exclusions.contains(o.getId())) {
                    collisions.add(o.getId());
                }
            } else {
                if (bounds.contains(o.getTransform().getPosition()) && !exclusions.contains(o.getId())) {
                    collisions.add(o.getId());
                }
            }
        }
        split.stop();
        return collisions;
    }

    @Override
    public Set<I> query(BoundingVolume bounds, Set<I> exclusions) {
        return query(bounds, exclusions, collisionMode);
    }

    @Override
    public Set<I> query(BoundingVolume bounds) {
        return query(bounds, Collections.<I> emptySet());
    }

    @Override
    public Transform query(I id) {
        Stopwatch stopwatch = SimonManager.getStopwatch("ReferenceSpatialManager.query(id)");
        Split split = stopwatch.start();
        if (spatials.containsKey(id)) {
            split.stop();
            return spatials.get(id).getTransform();
        }
        split.stop();
        return null;
    }

    @Override
    public void remove(I id) {
        SpatialObject existing = spatials.remove(id);

        if (null != existing) {

            Set<I> oldC = existing.getCollisions();

            if (oldC.size() > 0) {
                uncollide(id, oldC);
            }
        }

    }

    @Override
    public void addCollisionListener(CollisionListener<I> cl) {
        if (null != cl) {
            collisionListeners.add(cl);
        }
    }

    private void notifyListeners(CollisionEvent<I> e) {
        for (CollisionListener<I> listener : collisionListeners) {
            listener.collide(e);
        }
    }

    private void collide(I id, Set<I> collisions) {
        // could probably wrap in a single event
        for (I collision : collisions) {
            synchronized (this) {
                SpatialObject<I> collide = spatials.get(collision);

                if (null != collide) {
                    Set<I> newCollisions = Sets.<I> newHashSet();
                    newCollisions.add(id);
                    newCollisions.addAll(collide.getCollisions());
                    spatials.put(collide.getId(),
                            new SpatialObject<I>(collide.getId(), collide.getTransform(), collide.getBounds(),
                                    newCollisions));
                }
            }
            notifyListeners(new CollisionEvent<I>(Type.Enter, id, collision));
        }
    }

    private void uncollide(I id, Set<I> uncollisions) {
        // could probably wrap in a single event
        for (I uncollide : uncollisions) {
            synchronized (this) {
                SpatialObject<I> un = spatials.get(uncollide);

                if (null != un) {
                    Set<I> newCollisions = Sets.<I> newHashSet();
                    newCollisions.addAll(un.getCollisions());
                    newCollisions.remove(id);
                    spatials.put(un.getId(), new SpatialObject<I>(un.getId(), un.getTransform(), un.getBounds(),
                            newCollisions));
                }

            }
            notifyListeners(new CollisionEvent<I>(Type.Exit, id, uncollide));
        }
    }

    @Override
    public String toString() {
        return "ReferenceSpatialManager []";
    }

    @Override
    public Set<I> getManagedObjects() {
        return ImmutableSet.copyOf(spatials.keySet());
    }
}
